<!--
@license
Copyright 2017 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
-->

<!--
  input-pipeline-analyzer is a view within the tf-profile-dashboard.
  It shows an graphical analysis for tensorflow input pipeline on TPU by comparing the average step
  time on host and on device.
-->

<link rel="import" href="../vz-line-chart/vz-line-chart.html">

<dom-module id="input-pipeline-analyzer">
  <template>
    <style>
      .section-header {
        text-decoration: underline;
        font-weight: bold;
        font-size: 200%;
      }
      .sub-section-header {
        text-decoration: underline;
        font-weight: bold;
        font-size: 150%;
      }
      #summary-host {
        font-weight: bold; 
      }
      .highlighted-text {
        text-decoration: underline;
        font-weight: bold;
        font-size: 120%;
      }
      .stddev-text{
        font-style: italic;
        opacity: 0.7;
      }
      .left-pane {
        width: 30%;
      }
      .right-pane {
        width: 100%;
        height: 300px;
      }
      .table-style {
        table-layout: fixed;
        width: 95%;
      }
      .performance-table {
        width: 100%;
      }
      .x-axis-title {
        text-align: center;
        text-transform: capitalize;
      }
      .y-axis-title {
        writing-mode: bt-rl;
        transform: rotate(270deg);
        text-align: center;
        white-space: nowrap;
        text-transform: capitalize;
      }
      #recommendation_details {
        color: green;
      }
      #recommendation_details a {
        color: #ff33cc;
        text-decoration: underline;
      }
      #summary_conclusion{
        font-weight:bolder;
        font-style:italic;
        color: var(--summary-color, green);
      }
      #summary_nextstep{
        font-weight:bolder;
        font-style:italic;
        color:green;
      }
      #recommendation_title{
        font-weight:bolder;
        font-style:normal;
        color:black;
      }
    </style>
    <div>
      <div id="section_summary">
        <div id="title_summary">
          <p class="section-header"> Section 1: Summary of input-pipeline analysis</p>
        </div>
        <p><span id="summary_conclusion">[[_summary_conclusion]]</span></p>
        <p><span id="recommendation_title">Recommendation for next step: </span><span id="summary_nextstep">[[_summary_nextstep]]</span></p>
      </div>
      <div id="section_device_side_analysis">
        <div id="title_device_side_analysis">
          <p class="section-header"> Section 2: Device-side analysis details</p>
        </div>
        <div id="section_device_step_time">
          <div id="title_device_step_time">
            <p class="sub-section-header">Section 2.1: Device step time</p>
          </div>
          <table class="table-style">
            <tr>
              <td class="left-pane">
                <div><p class="highlighted-text">Device step-time statistics (in ms)</p></div>
                <p>
                  <span><b>Average:</b> </span>
                  <span>[[_steptime_ms_average]] ms </span>
                  <span class="stddev-text">(σ = [[_steptime_ms_stddev]] ms)</span>
                </p>
                <p>
                  <span><b>Range:</b> </span>
                  <span>[[_steptime_ms_minimum]] - [[_steptime_ms_maximum]] ms</span>
                </p>
              </td>
              <td><div class="y-axis-title">milliseconds</div></td>
              <td class="right-pane">
                <vz-line-chart id="device_step_chart" style="height:300px;"></vz-line-chart>
                <div><p class="x-axis-title">training step number</p></div>
              </td>
            </tr>
          </table>
        </div>
        <div id="section_device_infeeddeq_time">
          <div id="title_device_infeeddeq_time">
            <p class="sub-section-header">
              <span>Section 2.2: Range of device time waiting for input data </span>
              <span><i style="opacity:0.5">across cores</i> at each step </span>
            </p>
          </div>
          <table class="table-style">
            <tr>
              <td class="left-pane">
                <p>
                  <span class="highlighted-text">% of device step time waiting for input data</span>
                  <span> (average over the maximum waiting time across cores at each step)</span>
                </p>
                <p>
                  <span><b>Average:</b> </span>
                  <span>[[_infeed_percent_average]] % </span>
                  <span class="stddev-text">(σ = [[_infeed_percent_stddev]] %)</span>
                </p>
                <p>
                  <span><b>Range:</b> </span>
                  <span>[[_infeed_percent_minimum]] - [[_infeed_percent_maximum]] %</span>
                </p>
              </td>
              <td><div class="y-axis-title"><p>% of device step time</p></div></td>
              <td class="right-pane">
                <vz-line-chart id="device_infeed_chart" style="height:300px;"></vz-line-chart>
                <div><p class="x-axis-title">training step number</p></div>
              </td>
            </tr>
          </table>
        </div>
      </div>
      <div id="section_host_side_analysis">
        <div id="title_host_side_analysis">
          <p class="section-header">Section 3: Host-side analysis details</p>
        </div>
        <div hidden$="[[!_show_host_side_chart]]">
          <div style="height:300px;" id="host_side_chart"></div>
          <p>
            <span><b>What can be done to reduce above components of the host input time:</b></span>
            <div id="recommendation_details"></div>
          </p>
        </div>
        <div hidden$="[[_show_host_side_chart]]">
          Host side analysis can not be done without instrumentation.
        </div>
        <p> Click the "Show" button below to see the source data of the breakdown.</p>
        <button on-click="onClick">[[_toggle_button_text]]</button>
        <table class="performance-table" hidden$="[[!_show_host_side_table]]">
          <thead>
            <th>Input Op</th>
            <th>Count</th>
            <th>Total Time (in ms)</th>
            <th>Total Time (as % of total input-processing time)</th>
            <th>Total Self Time (in ms)</th>
            <th>Total Self Time (as % of total input-processing time)</th>
            <th>Category</th>
          </thead>
          <tbody id="host_side_table_content">
          </tbody>
        </table>
      </div>
    </div>
  </template>

  <script>
  Polymer({
    is: 'input-pipeline-analyzer',
    properties: {
      _data: {
        type: Object,
        observer: '_updateView',
      },
      _show_host_side_chart: {
        type: Boolean,
        value: true,
        notify: true,
      },
      _show_host_side_table: {
        type: Boolean,
        value: false,
        notify: true,
      },
      _toggle_button_text: {
        type: String,
        computed: '_getToggleButtonText(_show_host_side_table)'
      },
    },
    _summary_conclusion: String,
    _summary_nextstep: String, 
    _infeed_percent_average: String,
    _infeed_percent_stddev: String,
    _infeed_percent_minimum: String,
    _infeed_percent_maximum: String,
    _steptime_ms_average: String,
    _steptime_ms_stddev: String,
    _steptime_ms_minimum: String,
    _steptime_ms_maximum: String,
    onClick: function(e) {
      this.set('_show_host_side_table', !this._show_host_side_table);
    },
    _getToggleButtonText: function(show_host_side_table) {
      return (show_host_side_table ? 'Hide' : 'Show') + ' Input Op Statistics';
    },

    /* Update view according to new data */
    _updateView: function() {
      if (this._data == null) return;
      var deviceJson = this._data[0];
      var hostJson = this._data[2];
      var recommendationJson = this._data[3];
      var conclusion = deviceJson.p['summary_conclusion'];
      this.set('_summary_conclusion', conclusion);
      this.set('_summary_nextstep', deviceJson.p['summary_nextstep']);
      this.set('_infeed_percent_average', deviceJson.p['infeed_percent_average']);
      this.set('_infeed_percent_stddev', deviceJson.p['infeed_percent_standard_deviation']);
      this.set('_infeed_percent_minimum', deviceJson.p['infeed_percent_minimum']);
      this.set('_infeed_percent_maximum', deviceJson.p['infeed_percent_maximum']);
      this.set('_steptime_ms_average', deviceJson.p['steptime_ms_average']);
      this.set('_steptime_ms_stddev', deviceJson.p['steptime_ms_standard_deviation']);
      this.set('_steptime_ms_minimum', deviceJson.p['steptime_ms_minimum']);
      this.set('_steptime_ms_maximum', deviceJson.p['steptime_ms_maximum']);
      if (conclusion.includes('HIGHLY')) {
        this.customStyle['--summary-color'] =  'red';
      } else if (conclusion.includes('MODERATE')) {
        this.customStyle['--summary-color'] =  'orange';
      }
      this.updateStyles();
      this._showDeviceStepChart(deviceJson);
      this._showDeviceInfeedChart(deviceJson);
      this._showHostChart(hostJson);
      this._showHostTable(hostJson);
      this._makeRecommendations(recommendationJson);
    },

    /* Plots a graph of step time (in ms) against step number */
    _showDeviceStepChart : function(deviceJson) {
      var deviceStepSeries = [];
      var hostStepSeries = [];
      var maxStepTime = 0;
      var step = -1;
      deviceJson.rows.forEach(function(row, index) {
        step = step > 0 ? ++step : Number(row.c[0].v);
        deviceStepSeries.push({"scalar": row.c[1].v,
                               "step": step,
                               "tpu_step": Number(row.c[0].v),
                               "low_watermark": 0})
        hostStepSeries.push({"scalar": row.c[1].v + row.c[2].v,
                             "step": step,
                             "low_watermark": row.c[1].v})
        maxStepTime = Math.max(maxStepTime, row.c[1].v + row.c[2].v)
      })
      var chart = this.$.device_step_chart;
      if (chart) {
        chart.setVisibleSeries(['device step time', 'compute time']);
        chart.setSeriesData('device step time', hostStepSeries);
        chart.setSeriesData('compute time', deviceStepSeries);
        chart.defaultYRange = [0, maxStepTime * 1.1]
        chart.smoothingEnabled = false;
        chart.tooltipColumns = [
          { title: 'Name', evaluate: (d) => d.dataset.metadata().name },
          { title: 'Time(ms)', evaluate: (d) => (d.datum.scalar).toFixed(2) },
          { title: 'Step', evaluate: (d) => d.datum.step }];
        chart.fillArea = {
          higherAccessor: (d) => d.scalar,
          lowerAccessor: (d) => d.low_watermark,
        };
        chart.xAxisFormatter = d3.format('d');
      }
    },

    /* Plots a graph of infeed dequeue time (in % of the step time) against step number */
    _showDeviceInfeedChart: function (deviceJson) {
      var deviceInfeedSeries = [];
      var step = -1;
      deviceJson.rows.forEach(function(row, index) {
        step = step > 0 ? ++step : Number(row.c[0].v);
        deviceInfeedSeries.push({"step": step,
                                 "tpu_step": Number(row.c[0].v),
                                 "scalar": row.c[4].v,
                                 "min": row.c[5].v,
                                 "max": row.c[6].v});
      })
      var chart = this.$.device_infeed_chart;
      if (chart) {
        chart.setVisibleSeries(['input time%']);
        chart.setSeriesData('input time%', deviceInfeedSeries);
        chart.tooltipColumns = [
          { title: 'Name', evaluate: (d) => d.dataset.metadata().name },
          { title: 'Step', evaluate: (d) => d.datum.tpu_step },
          { title: 'Average(%)', evaluate: (d) => (d.datum.scalar).toFixed(4) + '%' },
          { title: 'Min(%)', evaluate: (d) => (d.datum.min).toFixed(4) + '%' },
          { title: 'Max(%)', evaluate: (d) => (d.datum.max).toFixed(4) + '%' }];
        chart.fillArea = {
          higherAccessor: (d) => d.max,
          lowerAccessor: (d) => d.min,
        };
        chart.xAxisFormatter = d3.format('d');
      }
    },

    /* Plots a stacked bar chart for the breakdown of input processing time on the host. */
    _showHostChart: function(hostJson) {
      const kUsPerMs = 1000.0;
      var unclassifiedNonEnqueueMs = Number(hostJson.p['unclassified_nonequeue_us']) / kUsPerMs;
      var demandedFileReadMs = Number(hostJson.p['demanded_file_read_us']) / kUsPerMs;
      var advancedFileReadMs = Number(hostJson.p['advanced_file_read_us']) / kUsPerMs;
      var preprocessingMs = Number(hostJson.p['preprocessing_us']) / kUsPerMs;
      var enqueueMs = Number(hostJson.p['enqueue_us']) / kUsPerMs;
      var totalInputTimeMs = unclassifiedNonEnqueueMs + demandedFileReadMs +
			     advancedFileReadMs + preprocessingMs + enqueueMs;
      this.set('_show_host_side_chart', totalInputTimeMs > 0);
      if (totalInputTimeMs > 0) {
	var pie_data = [{ Name:"Other data reading or processing",
			  Total:unclassifiedNonEnqueueMs / totalInputTimeMs * 100 },
			{ Name:"Reading data from files on demand",
			  Total:demandedFileReadMs / totalInputTimeMs * 100 },
			{ Name:"Reading data from files in advance [including caching, prefetching, interleaving]",
			  Total:advancedFileReadMs /totalInputTimeMs * 100 },
			{ Name:"Data preprocessing",
			  Total:preprocessingMs/totalInputTimeMs * 100 },
			{ Name:"Enqueuing data to be transferred to device",
			  Total:enqueueMs/totalInputTimeMs * 100 }];

        var colorScale = new Plottable.Scales.Color();
        var legend = new Plottable.Components.Legend(colorScale);

        var div = d3.select(this.$.host_side_chart);
        var pie = new Plottable.Plots.Pie()
          .attr("fill", function(d){ return d.Name; }, colorScale)
          .addDataset(new Plottable.Dataset(pie_data))
          .sectorValue(function(d){ return d.Total; } )
          .labelsEnabled(true)
          .labelFormatter(function(n){ return Number(n).toFixed(2) + "%" ;});
        var plot = new Plottable.Components.Table([[pie, legend]]);
        plot.renderTo(div);
      }
    },

    /* Draws a table of Dataset Op statistics */
    _showHostTable: function(hostJson) {
      var inputOpsTableBody = this.$.host_side_table_content;
      inputOpsTableBody.innerHTML = '';
      hostJson.rows.forEach(function(rowJson, index) {
        var row = document.createElement('tr');
        Polymer.dom(inputOpsTableBody).appendChild(row);
        /* formatting the cell */
        var cellTexts = [];
        cellTexts.push(rowJson.c[0].v);
        cellTexts.push(rowJson.c[1].v);
        cellTexts.push((rowJson.c[2].v).toFixed(2));
        cellTexts.push((rowJson.c[3].v * 100).toFixed(2) + '%');
        cellTexts.push((rowJson.c[4].v).toFixed(2));
        cellTexts.push((rowJson.c[5].v * 100).toFixed(2) + '%');
        cellTexts.push(rowJson.c[6].v);
        cellTexts.forEach(function(cellText, index) {
          var td = document.createElement('td');
          Polymer.dom(row).appendChild(td);
          Polymer.dom(td).appendChild(document.createTextNode(cellText));
        });
      });
      this.set('_show_host_side_table', false);
    },

    /* Makes a list of detailed recommendations */
    _makeRecommendations: function(recommendationJson) {
      var content = '';
      recommendationJson.rows.forEach(function(rowJson, index) {
	content += '<li>' + rowJson.c[0].v + '</li>';
      });
      var detailsBody = this.$.recommendation_details;
      detailsBody.innerHTML = content;
    },
  });
  </script>
</dom-module>
